TextLayoutBuilder源码解读

原理

背景

Canvas在drawText的时候,如果需要每次都计算字体的大小,边距之类的话,就会非常耗时,导致drawText时间会拉的很长,为了提高效率,android4.0之后引入了TextLayoutCache,使用了LRU缓存了字型,边距等数据,提升了drawText的速度,在4.4中,这个cache的大小是0.5m,全局使用,并且受到系统的控制会在Activity的configurationChanged, onResume, lowMemory, updateVisibility等时机,会调用Canvas.freeTextLayoutCache来释放这部分内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// /frameworks/minikin/libs/minikin/Layout.cpp中的LayoutCache
class LayoutCache : private OnEntryRemoved<LayoutCacheKey, Layout*> {
public:
LayoutCache() : mCache(kMaxEntries) {
mCache.setOnEntryRemovedListener(this);
}

void clear() {
mCache.clear();
}

Layout* get(LayoutCacheKey& key, LayoutContext* ctx, const FontCollection* collection) {
Layout* layout = mCache.get(key);
if (layout == NULL) {
key.copyText();
layout = new Layout();
key.doLayout(layout, ctx, collection);
mCache.put(key, layout);
}
return layout;
}

private:
// callback for OnEntryRemoved
void operator()(LayoutCacheKey& key, Layout*& value) {
key.freeText();
delete value;
}

LruCache<LayoutCacheKey, Layout*> mCache;

//static const size_t kMaxEntries = LruCache<LayoutCacheKey, Layout*>::kUnlimitedCapacity;

// TODO: eviction based on memory footprint; for now, we just use a constant
// number of strings
static const size_t kMaxEntries = 5000;
};


//................
//cache的复用规则
//..................
bool LayoutCacheKey::operator==(const LayoutCacheKey& other) const {
return mId == other.mId
&& mStart == other.mStart
&& mCount == other.mCount
&& mStyle == other.mStyle
&& mSize == other.mSize
&& mScaleX == other.mScaleX
&& mSkewX == other.mSkewX
&& mLetterSpacing == other.mLetterSpacing
&& mPaintFlags == other.mPaintFlags
&& mHyphenEdit == other.mHyphenEdit
&& mIsRtl == other.mIsRtl
&& mNchars == other.mNchars
&& !memcmp(mChars, other.mChars, mNchars * sizeof(uint16_t));
}
//............

setText

从以下源码可知,假如textView的宽高是wrap_content的话,会重新触发requestLayout方法,从measure开始重新来一遍否则从layout开始重新来一遍,所以是非常耗时的一个过程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
 private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//.............

if (mLayout != null) {
checkForRelayout();//重点关注 1
}

sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);

notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);

if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}

//......
}



/**
* Check whether entirely new text requires a new view layout(here from measure)
* or merely a new text layout(here from layout).
*/
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.

if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
(mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
(mHint == null || mHintLayout != null) &&
(mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.

int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);

if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
mLayoutParams.height != LayoutParams.MATCH_PARENT) {
invalidate();
return;
}

// Dynamic height, but height has stayed the same,
// so use our new text layout.
if (mLayout.getHeight() == oldht &&
(mHintLayout == null || mHintLayout.getHeight() == oldht)) {
invalidate();
return;
}
}

// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}

优化方向

  • 我们可以在View这一层根据文字内容,文字大小等信息封装一个可复用的TextLayoutBuilder这样就不需要重新Layout相同的内容
  • 根据4.0之后增加的LruCache原理,在子线程层创建一个warmUpHandler用于预热所要draw的Text,这样在ui线程真正要画它的时候,可以直接从cache中拿,起到加速的效果

TextLayoutBuilder

根据上面的两个优化点,所以有了facebook开源的TextLayoutBuilder项目

框架图

TextBuilderLayout

部分源码

如何build一个Layout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
TextLayoutBuilder.build()处理逻辑
* Builds and returns a {@link Layout}.
* //1.没有变化直接反馈
* //2.like link span do not cache this layout,so they could not get from cache
* //3.if numer ==1 使用boringlayout获得metric
* //4.计算desiredwidth,然后传给bringLayout或者staticLayout去make
*
* @return A {@link Layout} based on the parameters set
*/
public Layout build() {
// Return the cached layout if no property changed.
if (mShouldCacheLayout && mSavedLayout != null) {
return mSavedLayout;
}

if (TextUtils.isEmpty(mParams.text)) {
return null;
}

boolean hasClickableSpans = false;
int hashCode = -1;

if (mShouldCacheLayout && mParams.text instanceof Spannable) {
ClickableSpan[] spans = ((Spannable) mParams.text).getSpans(
0,
mParams.text.length() - 1,
ClickableSpan.class);
hasClickableSpans = spans.length > 0;
}

// If the text has ClickableSpans, it will be bound to different
// click listeners each time. It is unsafe to cache these text Layouts.
// Hence they will not be in cache.
if (mShouldCacheLayout && !hasClickableSpans) {
hashCode = mParams.hashCode();
Layout cachedLayout = sCache.get(hashCode);
if (cachedLayout != null) {
return cachedLayout;
}
}

BoringLayout.Metrics metrics = null;

int numLines = mParams.singleLine ? 1 : mParams.maxLines;

// Try creating a boring layout only if singleLine is requested.
if (numLines == 1) {
metrics = BoringLayout.isBoring(mParams.text, mParams.paint);
}

// getDesiredWidth here is used to ensure we layout text at the same size which it is measured.
// If we used a large static value it would break RTL due to drawing text at the very end of the
// large value.
int width;
switch (mParams.measureMode) {
case MEASURE_MODE_UNSPECIFIED:
width = (int) Math.ceil(Layout.getDesiredWidth(mParams.text, mParams.paint));
break;
case MEASURE_MODE_EXACTLY:
width = mParams.width;
break;
case MEASURE_MODE_AT_MOST:
width =
Math.min(
(int) Math.ceil(Layout.getDesiredWidth(mParams.text, mParams.paint)),
mParams.width);
break;
default:
throw new IllegalStateException("Unexpected measure mode " + mParams.measureMode);
}

final int lineHeight = mParams.getLineHeight();
if (mMaxWidthMode == EMS) {
//em是一个印刷排版的单位,表示字宽的单位。 em字面意思为:equal M (和M字符一致的宽度为一个单位)简称em。ems是em的复数表达
//mMaxWidth 使用的计量单位是行高
width = Math.min(width, mMaxWidth * lineHeight);
} else {
width = Math.min(width, mMaxWidth);
}

if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * lineHeight);
} else {
width = Math.max(width, mMinWidth);
}

Layout layout;
if (metrics != null) {
layout = BoringLayout.make(
mParams.text,
mParams.paint,
width,
mParams.alignment,
mParams.spacingMult,
mParams.spacingAdd,
metrics,
mParams.includePadding,
mParams.ellipsize,
width);
} else {
while (true) {
try {
layout = StaticLayoutHelper.make(
mParams.text,
0,
mParams.text.length(),
mParams.paint,
width,
mParams.alignment,
mParams.spacingMult,
mParams.spacingAdd,
mParams.includePadding,
mParams.ellipsize,
width,
numLines,
mParams.textDirection);
} catch (IndexOutOfBoundsException e) {
// Workaround for https://code.google.com/p/android/issues/detail?id=35412
if (!(mParams.text instanceof String)) {
// remove all Spannables and re-try
Log.e("TextLayoutBuilder", "Hit bug #35412, retrying with Spannables removed", e);
mParams.text = mParams.text.toString();
continue;
} else {
// If it still happens with all Spannables removed we'll bubble the exception up
throw e;
}
}

break;
}
}

// Do not cache if the text has ClickableSpans.
if (mShouldCacheLayout && !hasClickableSpans) {
mSavedLayout = layout;
sCache.put(hashCode, layout);
}

// Force a new paint.
mParams.mForceNewPaint = true;

if (mShouldWarmText && mGlyphWarmer != null) {
// Draw the text in a background thread to warm the cache.
mGlyphWarmer.warmLayout(layout);
}

return layout;
}

如何确定复用的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//cache的key值
@Override
public int hashCode() {
Typeface tf = getTypeface();

int hashCode = 1;
hashCode = 31 * hashCode + getColor();
hashCode = 31 * hashCode + Float.floatToIntBits(getTextSize());
hashCode = 31 * hashCode + (tf != null ? tf.hashCode() : 0);
hashCode = 31 * hashCode + Float.floatToIntBits(mShadowDx);
hashCode = 31 * hashCode + Float.floatToIntBits(mShadowDy);
hashCode = 31 * hashCode + Float.floatToIntBits(mShadowRadius);
hashCode = 31 * hashCode + mShadowColor;
hashCode = 31 * hashCode + linkColor;

// Array.
if (drawableState == null) {
hashCode = 31 * hashCode + 0;
} else {
for (int i = 0; i < drawableState.length; i++) {
hashCode = 31 * hashCode + drawableState[i];
}
}

return hashCode;
}

疑惑解答

staticLayout不支持maxLine的构造函数

faceBook的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
//编译替换?TODO
package android.text;

public class StaticLayout {
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Layout.Alignment align, TextDirectionHeuristic textDir,
float spacingmult, float spacingadd,
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
throw new RuntimeException("Stub!");
}
}

反射构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class StaticLayoutWithMaxLines {
private static final String LOGTAG = "StaticLayout";

private static final String TEXT_DIR_CLASS = "android.text.TextDirectionHeuristic";
private static final String TEXT_DIRS_CLASS = "android.text.TextDirectionHeuristics";
private static final String TEXT_DIR_FIRSTSTRONG_LTR = "FIRSTSTRONG_LTR";

private static boolean sInitialized;

private static Constructor<StaticLayout> sConstructor;
private static Object[] sConstructorArgs;
private static Object sTextDirection;

public static synchronized void ensureInitialized() {
if (sInitialized) {
return;
}

try {
final Class<?> textDirClass;
if (Build.VERSION.SDK_INT >= 18) {
textDirClass = TextDirectionHeuristic.class;
sTextDirection = TextDirectionHeuristics.FIRSTSTRONG_LTR;
} else {
final ClassLoader loader = StaticLayoutWithMaxLines.class.getClassLoader();
textDirClass = loader.loadClass(TEXT_DIR_CLASS);

final Class<?> textDirsClass = loader.loadClass(TEXT_DIRS_CLASS);
sTextDirection = textDirsClass.getField(TEXT_DIR_FIRSTSTRONG_LTR)
.get(textDirsClass);
}

final Class<?>[] signature = new Class[] {
CharSequence.class,
int.class,
int.class,
TextPaint.class,
int.class,
Alignment.class,
textDirClass,
float.class,
float.class,
boolean.class,
TruncateAt.class,
int.class,
int.class
};

// Make the StaticLayout constructor with max lines public
sConstructor = StaticLayout.class.getDeclaredConstructor(signature);
sConstructor.setAccessible(true);
sConstructorArgs = new Object[signature.length];
} catch (NoSuchMethodException e) {
Log.e(LOGTAG, "StaticLayout constructor with max lines not found.", e);
} catch (ClassNotFoundException e) {
Log.e(LOGTAG, "TextDirectionHeuristic class not found.", e);
} catch (NoSuchFieldException e) {
Log.e(LOGTAG, "TextDirectionHeuristics.FIRSTSTRONG_LTR not found.", e);
} catch (IllegalAccessException e) {
Log.e(LOGTAG, "TextDirectionHeuristics.FIRSTSTRONG_LTR not accessible.", e);
} finally {
sInitialized = true;
}
}

public static boolean isSupported() {
if (Build.VERSION.SDK_INT < 14) {
return false;
}

ensureInitialized();
return (sConstructor != null);
}

public static synchronized StaticLayout create(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerWidth, Alignment align,
float spacingMult, float spacingAdd,
boolean includePad, TruncateAt ellipsize,
int ellipsisWidth, int maxLines) {
ensureInitialized();

try {
sConstructorArgs[0] = source;
sConstructorArgs[1] = bufstart;
sConstructorArgs[2] = bufend;
sConstructorArgs[3] = paint;
sConstructorArgs[4] = outerWidth;
sConstructorArgs[5] = align;
sConstructorArgs[6] = sTextDirection;
sConstructorArgs[7] = spacingMult;
sConstructorArgs[8] = spacingAdd;
sConstructorArgs[9] = includePad;
sConstructorArgs[10] = ellipsize;
sConstructorArgs[11] = ellipsisWidth;
sConstructorArgs[12] = maxLines;

return sConstructor.newInstance(sConstructorArgs);
} catch (Exception e) {
throw new IllegalStateException("Error creating StaticLayout with max lines: " + e);
}
}
}

LinkageError的异常原因

//TODO 20171029

实际运用

Layout复用管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import android.graphics.Canvas;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

public class TextLayoutManager {
@SuppressWarnings("unused")
private static final String TAG = "TextLayoutManager";

private static final int MSG_INIT = 0;
private static final int MSG_EXECUTE = 1;

private final LruCache<String, Task> mCache = new LruCache<String, Task>(50);
private final ArrayList<Task> mTasks = new ArrayList<Task>();

private Handler mUIHandler;
private HandlerThread mHandlerThread;
private Handler mHandler;
private Canvas mDummyCanvas;

public TextLayoutManager() {
mUIHandler = new Handler(Looper.getMainLooper());

mHandlerThread = new HandlerThread("TextLayout_Warm");
mHandlerThread.start();
mHandler = new WarmHandler(mHandlerThread.getLooper());
mHandler.sendEmptyMessage(MSG_INIT);
}

public void destroy() {
mUIHandler = null;
if (mHandlerThread != null) {
Looper looper = mHandlerThread.getLooper();
if (looper != null) {
looper.quit();
}
mHandlerThread = null;
}
mHandler = null;
mCache.evictAll();
mTasks.clear();
mDummyCanvas = null;
}

public void getLayout(String textId, CharSequence text, int width, TextPaint textPaint, int maxLines, TextUtils.TruncateAt ellipsize, Layout.Alignment alignment, float spacingMult, OnGetLayoutListener listener) {
// Log.d(TAG, "getLayout, textId:" + textId + ", text:" + text + ", width:" + width);
if (!TextUtils.isEmpty(textId)) {
Task task = mCache.get(textId);

if (task == null || !task.isSameLayout(textId, width, textPaint, maxLines, ellipsize, alignment)) {
synchronized (mTasks) {
// mCache.get(id) may not be null now, will be checked when in execute process.
int taskSize = mTasks.size();
for (int i = 0; i < taskSize; i++) {
task = mTasks.get(i);
if (task.isSameLayout(textId, width, textPaint, maxLines, ellipsize, alignment)) {
task.listener = listener;
return;
}
}

mTasks.add(new Task(textId, text, width, textPaint, maxLines, ellipsize, alignment, spacingMult, listener));
// Log.d(TAG, "getLayout, add task, task.id:" + textId);
}
if (!mHandler.hasMessages(MSG_EXECUTE)) {
mHandler.sendEmptyMessage(MSG_EXECUTE);
}
} else if (listener != null) {
// Log.i(TAG, "onGetLayout, textId:" + task.id + ", text:" + task.text + ", width:" + task.width);
listener.onGetLayout(textId, task.layout);
}
}
}

public void cancelGetLayout(String textId) {
// Log.w(TAG, "cancelGetLayout, textId:" + textId);
synchronized (mTasks) {
int taskSize = mTasks.size();
Task task;
for (int i = 0; i < taskSize; i++) {
task = mTasks.get(i);
if (task.id.equals(textId)) {
mTasks.remove(i);
task.canceled.set(true);
// Log.w(TAG, "cancelGetLayout, remove task, task.id:" + task.id);
return;
}
}
}

Task task = mCache.get(textId);
if (task != null) {
task.canceled.set(true);
// Log.w(TAG, "cancelGetLayout, setCanceled, task.id:" + task.id);
}
}

private final class WarmHandler extends Handler {
WarmHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INIT:
mDummyCanvas = new Canvas();
break;

case MSG_EXECUTE:
Task task = null;
Task cacheTask = null;
Layout layout = null;
while (true) {
synchronized (mTasks) {
if (task != null) {
task.layout = layout;

// check whether the task has been canceled, if canceled, it should has been removed from mTasks
if (mTasks.size() > 0 && mTasks.get(0) == task) {
mTasks.remove(0);
// Log.d(TAG, "MSG_EXECUTE, remove task, task.id:" + task.id);
}

if (task.listener != null && !task.canceled.get()) {
final OnGetLayoutListener finalListener = task.listener;
final Task finalTask = task;
mUIHandler.post(new Runnable() {
@Override
public void run() {
if (!finalTask.canceled.get()) {
// Log.i(TAG, "onGetLayout, textId:" + finalTask.id + ", text:" + finalTask.text + ", width:" + finalTask.width);
finalListener.onGetLayout(finalTask.id, finalTask.layout);
}
}
});
task.listener = null;
}

mCache.put(task.id, task);
}
task = mTasks.size() > 0 ? mTasks.get(0) : null;
}

if (task == null) {
return;
} else {
cacheTask = mCache.get(task.id);
if (cacheTask == null || !task.isSameLayout(cacheTask) || cacheTask.layout == null) {
layout = StaticLayoutWithMaxLines.create(task.text, 0, task.text.length(), task.textPaint, task.width, task.alignment, task.spacingMult, 0f, true, task.ellipsize, task.width, task.maxLines);
layout.draw(mDummyCanvas);
} else {
layout = cacheTask.layout;
}
}
}
}
}
}

static class Task {
String id;
CharSequence text;
int width;
TextPaint textPaint;
int maxLines;
TextUtils.TruncateAt ellipsize;
Layout.Alignment alignment;
float spacingMult;

OnGetLayoutListener listener;
Layout layout;

AtomicBoolean canceled;

public Task(String id, CharSequence text, int width, TextPaint textPaint, int maxLines, TextUtils.TruncateAt ellipsize, Layout.Alignment alignment, float spacingMult, OnGetLayoutListener listener) {
this.id = id;
this.text = text;
this.width = width;
this.textPaint = textPaint;
this.maxLines = maxLines;
this.ellipsize = ellipsize;
this.alignment = alignment;
this.spacingMult = spacingMult;
this.listener = listener;
this.layout = null;
this.canceled = new AtomicBoolean(false);
}

public boolean isSameLayout(Task other) {
return other != null && this.id.equals(other.id) && this.width == other.width && this.textPaint == other.textPaint && this.maxLines == other.maxLines
&& this.ellipsize == other.ellipsize && this.alignment == other.alignment && this.spacingMult == other.spacingMult;
}

public boolean isSameLayout(String id, int width, TextPaint textPaint, int maxLines, TextUtils.TruncateAt ellipsize, Layout.Alignment alignment) {
return this.id.equals(id) && this.width == width && this.textPaint == textPaint && this.maxLines == maxLines
&& this.ellipsize == ellipsize && this.alignment == alignment && this.spacingMult == spacingMult;
}
}

public interface OnGetLayoutListener {
void onGetLayout(String textId, Layout layout);
}
}

如何封装view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import android.graphics.Canvas;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

public class TextLayoutManager {
@SuppressWarnings("unused")
private static final String TAG = "TextLayoutManager";

private static final int MSG_INIT = 0;
private static final int MSG_EXECUTE = 1;

private final LruCache<String, Task> mCache = new LruCache<String, Task>(50);
private final ArrayList<Task> mTasks = new ArrayList<Task>();

private Handler mUIHandler;
private HandlerThread mHandlerThread;
private Handler mHandler;
private Canvas mDummyCanvas;

public TextLayoutManager() {
mUIHandler = new Handler(Looper.getMainLooper());

mHandlerThread = new HandlerThread("TextLayout_Warm");
mHandlerThread.start();
mHandler = new WarmHandler(mHandlerThread.getLooper());
mHandler.sendEmptyMessage(MSG_INIT);
}

public void destroy() {
mUIHandler = null;
if (mHandlerThread != null) {
Looper looper = mHandlerThread.getLooper();
if (looper != null) {
looper.quit();
}
mHandlerThread = null;
}
mHandler = null;
mCache.evictAll();
mTasks.clear();
mDummyCanvas = null;
}

public void getLayout(String textId, CharSequence text, int width, TextPaint textPaint, int maxLines, TextUtils.TruncateAt ellipsize, Layout.Alignment alignment, float spacingMult, OnGetLayoutListener listener) {
// Log.d(TAG, "getLayout, textId:" + textId + ", text:" + text + ", width:" + width);
if (!TextUtils.isEmpty(textId)) {
Task task = mCache.get(textId);

if (task == null || !task.isSameLayout(textId, width, textPaint, maxLines, ellipsize, alignment)) {
synchronized (mTasks) {
// mCache.get(id) may not be null now, will be checked when in execute process.
int taskSize = mTasks.size();
for (int i = 0; i < taskSize; i++) {
task = mTasks.get(i);
if (task.isSameLayout(textId, width, textPaint, maxLines, ellipsize, alignment)) {
task.listener = listener;
return;
}
}

mTasks.add(new Task(textId, text, width, textPaint, maxLines, ellipsize, alignment, spacingMult, listener));
// Log.d(TAG, "getLayout, add task, task.id:" + textId);
}
if (!mHandler.hasMessages(MSG_EXECUTE)) {
mHandler.sendEmptyMessage(MSG_EXECUTE);
}
} else if (listener != null) {
// Log.i(TAG, "onGetLayout, textId:" + task.id + ", text:" + task.text + ", width:" + task.width);
listener.onGetLayout(textId, task.layout);
}
}
}

public void cancelGetLayout(String textId) {
// Log.w(TAG, "cancelGetLayout, textId:" + textId);
synchronized (mTasks) {
int taskSize = mTasks.size();
Task task;
for (int i = 0; i < taskSize; i++) {
task = mTasks.get(i);
if (task.id.equals(textId)) {
mTasks.remove(i);
task.canceled.set(true);
// Log.w(TAG, "cancelGetLayout, remove task, task.id:" + task.id);
return;
}
}
}

Task task = mCache.get(textId);
if (task != null) {
task.canceled.set(true);
// Log.w(TAG, "cancelGetLayout, setCanceled, task.id:" + task.id);
}
}

private final class WarmHandler extends Handler {
WarmHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INIT:
mDummyCanvas = new Canvas();
break;

case MSG_EXECUTE:
Task task = null;
Task cacheTask = null;
Layout layout = null;
while (true) {
synchronized (mTasks) {
if (task != null) {
task.layout = layout;

// check whether the task has been canceled, if canceled, it should has been removed from mTasks
if (mTasks.size() > 0 && mTasks.get(0) == task) {
mTasks.remove(0);
// Log.d(TAG, "MSG_EXECUTE, remove task, task.id:" + task.id);
}

if (task.listener != null && !task.canceled.get()) {
final OnGetLayoutListener finalListener = task.listener;
final Task finalTask = task;
mUIHandler.post(new Runnable() {
@Override
public void run() {
if (!finalTask.canceled.get()) {
// Log.i(TAG, "onGetLayout, textId:" + finalTask.id + ", text:" + finalTask.text + ", width:" + finalTask.width);
finalListener.onGetLayout(finalTask.id, finalTask.layout);
}
}
});
task.listener = null;
}

mCache.put(task.id, task);
}
task = mTasks.size() > 0 ? mTasks.get(0) : null;
}

if (task == null) {
return;
} else {
cacheTask = mCache.get(task.id);
if (cacheTask == null || !task.isSameLayout(cacheTask) || cacheTask.layout == null) {
layout = StaticLayoutWithMaxLines.create(task.text, 0, task.text.length(), task.textPaint, task.width, task.alignment, task.spacingMult, 0f, true, task.ellipsize, task.width, task.maxLines);
layout.draw(mDummyCanvas);
} else {
layout = cacheTask.layout;
}
}
}
}
}
}

static class Task {
String id;
CharSequence text;
int width;
TextPaint textPaint;
int maxLines;
TextUtils.TruncateAt ellipsize;
Layout.Alignment alignment;
float spacingMult;

OnGetLayoutListener listener;
Layout layout;

AtomicBoolean canceled;

public Task(String id, CharSequence text, int width, TextPaint textPaint, int maxLines, TextUtils.TruncateAt ellipsize, Layout.Alignment alignment, float spacingMult, OnGetLayoutListener listener) {
this.id = id;
this.text = text;
this.width = width;
this.textPaint = textPaint;
this.maxLines = maxLines;
this.ellipsize = ellipsize;
this.alignment = alignment;
this.spacingMult = spacingMult;
this.listener = listener;
this.layout = null;
this.canceled = new AtomicBoolean(false);
}

public boolean isSameLayout(Task other) {
return other != null && this.id.equals(other.id) && this.width == other.width && this.textPaint == other.textPaint && this.maxLines == other.maxLines
&& this.ellipsize == other.ellipsize && this.alignment == other.alignment && this.spacingMult == other.spacingMult;
}

public boolean isSameLayout(String id, int width, TextPaint textPaint, int maxLines, TextUtils.TruncateAt ellipsize, Layout.Alignment alignment) {
return this.id.equals(id) && this.width == width && this.textPaint == textPaint && this.maxLines == maxLines
&& this.ellipsize == ellipsize && this.alignment == alignment && this.spacingMult == spacingMult;
}
}

public interface OnGetLayoutListener {
void onGetLayout(String textId, Layout layout);
}
}

TODO

LinkageError的异常原因